这一篇笔记的重点在于探究 Vue 的响应式特性,并动手实现一个简易的响应系统。
什么是响应式
在前端框架这个语境下是指,状态的改变引起相应的 DOM 随之改变。
我们先从一个最简单的情形说起,假如有 2 个变量 a
和 b
,我们要实现的需求是,让变量 b
的值总是变量 a
的 10 倍。
1 | let a = 3; |
上面代码中,每次变量 a
发生变化,必须手动更新变量 b
的值,才能使得两者始终保持 10 倍的关系。如何声明式地表示这种关系呢?
在 Excel 中,我们是通过函数来实现类似效果的:
No. | A | B |
---|---|---|
1 | 4 | 40(fx = A1 * 10 ) |
在程序语言中,我们想要的是一个类似这样的函数:
1 | onAChanged(() => { |
每当变量 a
的值发生改变,作为回调函数参数的这个更新函数便执行(即上面代码中的箭头函数),这样问题就解决了。那么如何实现这样一个函数?
先将前面问题稍微转化一下,使之更符合 Web 开发的实际情况。
我们现在有一个 <span>
标签 b1
,它的值是状态变量 a
的 10 倍:
1 | <span class="cell b1" /> |
1 | onStateChanged(() => { |
上面的代码实际上声明式地表达了状态与 DOM 之间的关系,不过我们可以更进一步抽象成这样:
1 | <span class="cell b1"> {{ state.a * 10 }} </span> |
1 | onStateChanged((state) => { |
从上面的代码我们隐约看到了一个 UI 库的雏形,最关键的是这行代码:view = render(state)
,它实际上高度抽象地概括了现代前端框架存在的根本原因:通过一种映射,将应用程序的 UI 与状态同步。这里面涉及到很多虚拟 DOM 和原生 DOM 的细节,所以我们这里先关注外面的回调函数即 onStateChanged
是如何实现的。
它可能是这样实现的:
1 | let update; |
上面代码中,我们将 update
函数保存在某处,同时要求用户总是通过调用一个函数 setState
来更新状态,而不是任意地操作状态。setState
函数所做的工作如下:将旧状态替换为新状态,然后调用 update
函数。这一过程与 React 的响应式系统很像,在应用状态改变时要求用户手动调用 setState
函数:
1 | onStateChanged(() => { |
而在 Vue 中,用户可以直接操作状态,状态改变会自动触发 onStateChanged
内更新函数的执行,不再需要用户手动调用 setState
。这是通过 ES5 的全局 API Object.defineProperty()
来使得状态具有响应特性。
Getter / Setter
先热下身,利用 Object.defineProperty()
动手实现一个 convert()
函数,需求如下:
- 接受一个对象类型的参数
- 将该对象的属性就地转化为
getter/setter
- 该对象应该保持原有的行为,同时在被访问和修改时打印
get/set
操作
1 | // 实现 |
Dependency Tracking
很明显,以上所做的还不能实现预期的效果,我们还需要利用发布/订阅模式实现 Dependency Tracking 依赖追踪。
需求如下:
- 创建一个
Dep
类,这个类有两个方法depend()
和notify
- 创建一个
autorun
函数,它接受一个update
更新函数作为参数 - 在
update
更新函数内部,你可以通过调用dep.depend()
显式地依赖一个Dep
类的实例 - 在这之后,你可以通过调用
dep.notify()
使得update
更新函数再次触发被调用
1 | // 实现 |
Mini Data Observer
将前面实现的 convert()
和 autorun()
这 2 个函数结合起来,同时将 convert()
函数重命名为 observe()
。实现的需求如下:
observe
函数接受一个对象类型的参数,并将该对象的所有属性转化成响应式的。每个被转化的属性都被分配到一个 Dep 实例,该实例跟踪着一个订阅了更新函数的列表,每当某个属性的setter
被调用,就重新调用订阅的更新函数。autorun
函数接受一个update
函数作为参数,同时在update
函数所订阅的属性被修改时,update
函数将被触发执行。如果update
更新函数在执行期间依赖于某个属性,则该更新函数订阅了该属性。
1 | <div> |
上面的代码实现了一个简单的 Data Observer,同时需要指出,有一些边缘情况没有考虑在内,比如清除陈旧的依赖、对数组的处理、对新添加属性的处理。